信号处理函数

  • signal函数每次设置的信号处理函数只能生效一次,每次在进程响应处理信号时,随即将信号处理函数恢复为默认处理方式,所以如果想多次相同方式处理某个信号,通常的做法是在响应函数开始时,再次调用signal设置
  • 而对于sigaction函数,在信号处理程序调用时,系统建立的新信号屏蔽字会自动包括正被递送的信号,因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会阻塞到对一个信号的处理结束为止;并且响应函数设置后会一直生效,不会重置
  • 如果希望能用相同方式处理信号的多次出现,最好用sigaction,而如果信号只出现并处理一次,可以用signal

TCP回射服务器程序:main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "unp.h"
int main(int argc, char** argv) {
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for (; ;) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);
if ((childpid = Fork()) == 0) {
Close(listenfd);
str_echo(connfd);
exit(0);
}
Close(connfd);
}
}

上述程序的流程如下:

  • 创建套接字,在待捆绑到该TCP套接字的网际网套接字地址结构中填入通配地址(INADDR_ANY)和服务器的众所周知端口(SERV_PORT,9877),捆绑通配地址是告诉系统,要是系统是多宿主机,我们将接受目的地址为任何本地接口的连接
  • 服务器阻塞于accept等待客户连接的完成
  • fork为每个客户派生一个处理它们的子进程,子进程关闭监听套接字,而父进程关闭连接套接字,子进程接着调用str_echo处理客户

TCP回射服务器程序:str_echo函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "unp.h"
void str_echo(int sockfd) {
ssize_t n;
char buf[MAXLINE];
again:
while((n = read(sockfd, buf, MAXLINE)) > 0) {
Writen(sockfd, buf, n);
}
if (n < 0 && errno == EINTR) { // EINTR:由于信号中断,没有读到任何数据
goto again;
} else if (n < 0) {
err_sys("str_echo: read error");
}
}

TCP回射客户程序:main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "unp.h"
int main(int argc, char** argv) {
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2) {
err_quit("usage: tcpcli <IPaddress>");
}
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
srevaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));
str_cli(stdin, sockfd);
exit(0);
}

上述程序的流程如下:

  • 创建套接字,用服务器的IP地址和端口号装填一个网际网套接字地址结构
  • connect服务器,str_cli函数完成剩余部分的客户处理工作

TCP回射客户程序:str_cli函数

1
2
3
4
5
6
7
8
9
10
11
#include " unp.h"
void str_cli(FILE* fp, int sockfd) {
char sendline[MAXLINE], recvline[MAXLINE];
while(Fgets(sendline, MAXLINE, fp) != NULL) {
Writen(sockfd, sendline, strlen(sendline));
if (Readline(sockfd, recvline, MAXLINE) == 0) {
err_quit("str_cli: server terminated prematurely");
}
Fputs(recvline, stdout);
}
}

上述程序的流程如下:

  • fgets读入一行文本,writen把该行发送给服务器
  • readline从服务器读入回射行,fputs把它写到标准输出

正常启动

当使用ps命令检查进程的状态时,Linux在进程阻塞于accept或connect时,WCHAN列输出wait_for_connect,在进程阻塞于套接字输入或输出时,输出tcp_data_wait,在进程阻塞于终端I/O时,输出read_chan

正常终止

  • 对于上面的客户端和服务器程序,当我们在客户端输入EOF字符时,fgets返回一个空指针,于是str_cli函数返回
  • 当str_cli函数返回到客户的main函数时,main通过调用exit终止
  • 进程终止处理的部分工作是关闭所有打开的描述符,因此客户打开的套接字由内核关闭,这导致客户TCP发送一个FIN给服务器,服务器TCP则以ACK响应,这就是TCP连接终止序列的前半部分,至此,服务器套接字处于CLOSE_WAIT状态,客户套接字处于FIN_WAIT_2状态
  • 当服务器TCP收到FIN时,服务器子进程阻塞于readline调用,于是readline返回0,这导致str_echo函数返回服务器子进程的main函数
  • 服务器子进程通过调用exit来终止
  • 服务器子进程中打开的所有描述符随之关闭,由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节:一个从服务器到客户的FIN和一个从客户到服务器的ACK,至此,连接完全终止,客户套接字进入TIME_WAIT状态。
  • 在服务器子进程终止时,给父进程发送一个SIGCHLD信号

POSIX信号处理

信号可以由一个进程发给另一个进程,也可以由内核发给某个进程。我们可以同调用sigaction函数来设定一个信号的处置。

  • 可以提供一个函数,只要在特定信号发生时它就被调用,这种行为成为捕获信号,有两个信号不能被捕获,分别是SIGKILL和SIGSTOP
  • 可以把某个信号的处置设置为SIG_IGN来忽略它,SIGKILL和SIGSTOP两个信号不能被忽略
  • 可以将某个信号的处置设置为SIG_DFL来启用它的默认设置

建立信号处理的POSIX方法是调用sigaction函数,可以像下面这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "unp.h"
Sigfunc* signal(int signo, Sigfunc* func) {
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART;
#endif
}
if (sigaction(signo, &act, &oact) < 0) {
return (SIR_ERR);
}
return oact.sa_handler;
}
  • 用typedef 简化函数模型

    函数signal的正常函数原型为:

    1
    void (*signal(int signo, void (func)(int)))(int);

    我们定义如下的Sigfunc函数,这样signal的函数原型变为:

    1
    2
    typedef void Sigfunc(int);
    Sigfunc* signal(int, Sigfunc* func);
  • typedef定义函数类型和函数指针

  • typedef定义函数指针类型:

    1
    typedef int (*fp_t)(char c);

    需要注意的是函数名本身就是一个指针,所以函数名和fp_t之间可以相互赋值

  • typedef定义函数类型

    1
    typedef int fp_t(char);

    此时不能直接将函数名赋值给fp_t,而只能是赋值个fp_t*。

  • 对于上面的程序段解释如下:

  • 设置处理函数:

    sigaction结构的sa_handler成员被置为func参数。

  • 设置处理函数的信号掩码:

    POSIX允许指定一组信号,它们在信号处理函数被调用时被阻塞,任何阻塞的信号都不能递交给进程,把sa_mask成员设置为空集,意味着在该信号处理函数运行期间,不阻塞额外的信号

  • SA_RESTART标志

    SA_RESTART表示由此信号中断的系统调用会自动重启,而如果设置为SA_INTERRUPT,则表示由此信号中断的系统调用不会自动重启

处理SIGCHLD信号

设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取,这些信息包括子进程的进程ID、终止状态以及资源利用时间(CPU时间、内存使用量等)。如果一个进程终止,而该进程所有子进程处于僵死状态,那么它的所有僵死子进程的父进程ID被重置为1(init进程),继承这些子进程的init进程将清理它们(init进程将wait它们,从而去除它们的僵死状态)。

  • 处理僵死进程

    无论何时fork子进程都得wait它们,以防它们变成僵死进程,可以建立一个俘获SIGCHLD信号的信号处理函数,在函数体中调用wait。

    1
    2
    3
    4
    5
    6
    7
    void sig_chld(int signo) {
    pid_t pid;
    int stat;
    pid = wait(&stat);
    printf("child %d terminated\n", pid);
    return;
    }
  • 处理被中断的系统调用

    当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误,有些内核自动重启某些被中断的系统调用。因此,为了便于移植,当我们编写捕获信号的程序时,我们必须对慢系统调用返回EINTR有所准备。

    为了处理被中断的accept,可以将accept的调用放入for循环中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for (; ;) {
    clilen = sizeof(chiaddr);
    if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) {
    if (errno == EINTR) {
    continue;
    } else {
    err_sys("accept error");
    }
    }
    }

    这段代码所做的事情就是自己重启被中断的系统调用,对于accept、read、write、select和open之类函数时合适的,不过有一个函数不能重启:connect。如果该函数返回EINTR,不能再次调用它,否则立即返回一个错误。当connect被一个捕获的信号中断而且不自动重启时,我们必须调用select来等待连接完成。

wait和waitpid

这两个函数的原型如下;

1
2
3
#include <sys/wait.h>
pid_t wait(int* staloc);
pid_t waitpid(pid_t pid, int* staloc, int options);

如果调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到现有子进程第一个终止为止。

waitpid函数就等待哪个进程以及是否阻塞给了我们更多的控制,pid参数允许我们指定想等待的进程ID,值-1表示等待第一个终止的子进程;options参数允许我们制定附加选项,最常用的选项是WNOHANG,告知内核在没有已终止子进程时不要阻塞。